Preprocesamiento de datos#

La limpieza de datos es un paso crítico en el desarrollo de modelos de machine learning, ya que garantiza que los datos utilizados para entrenar y evaluar los modelos sean precisos, confiables y representativos, lo que a su vez mejora la calidad y la eficacia de los modelos resultantes.

import pandas as pd
from numpy import NaN
import numpy as np
import requests
import plotly.express as px
import os
df = pd.read_parquet("Data/raw_data.parquet")
df.head()
departamento municipio armas_medios fecha_hecho genero grupo_etario cantidad
0 ATLÁNTICO BARRANQUILLA (CT) ARMA BLANCA / CORTOPUNZANTE 1/01/2010 MASCULINO ADULTOS 1
1 BOYACÁ DUITAMA ARMA BLANCA / CORTOPUNZANTE 1/01/2010 FEMENINO ADULTOS 1
2 CAQUETÁ PUERTO RICO ARMA BLANCA / CORTOPUNZANTE 1/01/2010 MASCULINO ADULTOS 1
3 CASANARE MANÍ ARMA BLANCA / CORTOPUNZANTE 1/01/2010 FEMENINO ADULTOS 1
4 CUNDINAMARCA BOGOTÁ D.C. (CT) ARMA BLANCA / CORTOPUNZANTE 1/01/2010 FEMENINO ADULTOS 1

Ajustes para columnas, valores nulos y duplicados

df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 575721 entries, 0 to 575720
Data columns (total 7 columns):
 #   Column        Non-Null Count   Dtype 
---  ------        --------------   ----- 
 0   departamento  575721 non-null  object
 1   municipio     575721 non-null  object
 2   armas_medios  575721 non-null  object
 3   fecha_hecho   575721 non-null  object
 4   genero        575721 non-null  object
 5   grupo_etario  574110 non-null  object
 6   cantidad      575721 non-null  int64 
dtypes: int64(1), object(6)
memory usage: 30.7+ MB

Tratamiento de Datos Faltantes y Atípicos#

df.isnull().sum()
departamento       0
municipio          0
armas_medios       0
fecha_hecho        0
genero             0
grupo_etario    1611
cantidad           0
dtype: int64
fig1 = px.box(df, x = 'grupo_etario', y = 'cantidad', title="Cantidad de Casos por Grupo Etario", notched = False)
fig1

Este gráfico muestra la distribución de los casos por grupos etarios (adultos, adolescentes, menores y no reportados). Lo más notable es el pico extremadamente alto en la categoría “ADULTOS”, que indica que este grupo representa una proporción muy significativa de los casos totales en comparación con los demás grupos. En contraste, los grupos de “ADOLESCENTES”, “MENORES” y “NO REPORTADO” tienen valores mucho más bajos, sugiriendo que tienen una presencia mucho menor en el total de casos.

fig2 = px.box(df, x = 'genero', y = 'cantidad', title="Cantidad de Casos por Género", notched = True)
fig2
fig3 = px.box(df, x = 'armas_medios', y = 'cantidad',title= "Cantidad de Casos por Tipo de Arma" ,notched = True)
fig3
fig4 = px.box(df, x = 'departamento', y = 'cantidad',title= "Cantidad de Casos por Departamento" ,notched = True)
fig4
# Se reemplaza 'NO REPORTA' con valores NaN
df.replace(['NO REPORTA', 'NO REPORTADO', '-'], np.nan, inplace=True)
empty_info = df.isnull().sum()*100/df.shape[0]
print(f'{empty_info}\n\n{df.isnull().sum()}')
departamento     0.001042
municipio        0.001390
armas_medios    13.261979
fecha_hecho      0.000000
genero           0.092580
grupo_etario     0.367887
cantidad         0.000000
dtype: float64

departamento        6
municipio           8
armas_medios    76352
fecha_hecho         0
genero            533
grupo_etario     2118
cantidad            0
dtype: int64

Imputación de valores faltantes con media o moda#

df['grupo_etario'].fillna(df['grupo_etario'].mode()[0], inplace=True)
df['departamento'].fillna(df['departamento'].mode()[0], inplace=True)
df['municipio'].fillna(df['municipio'].mode()[0], inplace=True)
df['armas_medios'].fillna('Unknown', inplace=True)
df['genero'].fillna(df['genero'].mode()[0], inplace=True)
C:\Users\Andres\AppData\Local\Temp\ipykernel_19416\3546716318.py:1: FutureWarning:

A value is trying to be set on a copy of a DataFrame or Series through chained assignment using an inplace method.
The behavior will change in pandas 3.0. This inplace method will never work because the intermediate object on which we are setting values always behaves as a copy.

For example, when doing 'df[col].method(value, inplace=True)', try using 'df.method({col: value}, inplace=True)' or df[col] = df[col].method(value) instead, to perform the operation inplace on the original object.



C:\Users\Andres\AppData\Local\Temp\ipykernel_19416\3546716318.py:2: FutureWarning:

A value is trying to be set on a copy of a DataFrame or Series through chained assignment using an inplace method.
The behavior will change in pandas 3.0. This inplace method will never work because the intermediate object on which we are setting values always behaves as a copy.

For example, when doing 'df[col].method(value, inplace=True)', try using 'df.method({col: value}, inplace=True)' or df[col] = df[col].method(value) instead, to perform the operation inplace on the original object.



C:\Users\Andres\AppData\Local\Temp\ipykernel_19416\3546716318.py:3: FutureWarning:

A value is trying to be set on a copy of a DataFrame or Series through chained assignment using an inplace method.
The behavior will change in pandas 3.0. This inplace method will never work because the intermediate object on which we are setting values always behaves as a copy.

For example, when doing 'df[col].method(value, inplace=True)', try using 'df.method({col: value}, inplace=True)' or df[col] = df[col].method(value) instead, to perform the operation inplace on the original object.



C:\Users\Andres\AppData\Local\Temp\ipykernel_19416\3546716318.py:4: FutureWarning:

A value is trying to be set on a copy of a DataFrame or Series through chained assignment using an inplace method.
The behavior will change in pandas 3.0. This inplace method will never work because the intermediate object on which we are setting values always behaves as a copy.

For example, when doing 'df[col].method(value, inplace=True)', try using 'df.method({col: value}, inplace=True)' or df[col] = df[col].method(value) instead, to perform the operation inplace on the original object.



C:\Users\Andres\AppData\Local\Temp\ipykernel_19416\3546716318.py:5: FutureWarning:

A value is trying to be set on a copy of a DataFrame or Series through chained assignment using an inplace method.
The behavior will change in pandas 3.0. This inplace method will never work because the intermediate object on which we are setting values always behaves as a copy.

For example, when doing 'df[col].method(value, inplace=True)', try using 'df.method({col: value}, inplace=True)' or df[col] = df[col].method(value) instead, to perform the operation inplace on the original object.
# Validación de cambios
print(df.isnull().sum())
departamento    0
municipio       0
armas_medios    0
fecha_hecho     0
genero          0
grupo_etario    0
cantidad        0
dtype: int64

Eliminando duplicados

df = df.loc[:, ~df.columns.duplicated()].copy()

Cambio tipo de datos

df['genero'] = df['genero'].astype('category')
df['grupo_etario'] = df['grupo_etario'].astype('category')
df['armas_medios'] = df['armas_medios'].astype('category')
df['fecha_hecho'] = pd.to_datetime(df['fecha_hecho'], format = '%d/%m/%Y')
df.dtypes
departamento            object
municipio               object
armas_medios          category
fecha_hecho     datetime64[ns]
genero                category
grupo_etario          category
cantidad                 int64
dtype: object

Se modifica la codificación de la columna departamento para que no haya errores

df.loc[:, 'departamento'] = df['departamento'].str.normalize('NFKD').str.encode('ascii', errors = 'ignore').str.decode('utf-8')
df['departamento'].replace({'SAN ANDRES':'ARCHIPIELAGO DE SAN ANDRES PROVIDENCIA Y SANTA CATALINA',
                             'VALLE':'VALLE DEL CAUCA',
                             'NARINO':'NARIÑO',
                             'GUAJIRA':'LA GUAJIRA'}, inplace = True)
df.departamento.unique()
C:\Users\Andres\AppData\Local\Temp\ipykernel_19416\3136357760.py:1: FutureWarning:

A value is trying to be set on a copy of a DataFrame or Series through chained assignment using an inplace method.
The behavior will change in pandas 3.0. This inplace method will never work because the intermediate object on which we are setting values always behaves as a copy.

For example, when doing 'df[col].method(value, inplace=True)', try using 'df.method({col: value}, inplace=True)' or df[col] = df[col].method(value) instead, to perform the operation inplace on the original object.
array(['ATLANTICO', 'BOYACA', 'CAQUETA', 'CASANARE', 'CUNDINAMARCA',
       'SUCRE', 'VALLE DEL CAUCA', 'HUILA', 'ANTIOQUIA', 'ARAUCA',
       'BOLIVAR', 'CALDAS', 'CAUCA', 'CESAR', 'CHOCO', 'CORDOBA',
       'MAGDALENA', 'META', 'NARIÑO', 'NORTE DE SANTANDER', 'PUTUMAYO',
       'RISARALDA', 'SANTANDER', 'TOLIMA', 'VAUPES', 'GUAVIARE',
       'LA GUAJIRA', 'QUINDIO', 'AMAZONAS', 'VICHADA', 'GUAINIA',
       'ARCHIPIELAGO DE SAN ANDRES PROVIDENCIA Y SANTA CATALINA'],
      dtype=object)
df.loc[df['municipio'] == 'BOGOTÁ D.C. (CT)', 'departamento'] = 'SANTAFE DE BOGOTA D.C'
df['armas_medios'] = df['armas_medios'].replace({'ARMA BLANCA / CORTOPUNZANTE':'ARMA BLANCA',
                            'CORTOPUNZANTES':'ARMA BLANCA',
                            'CORTANTES':'ARMA BLANCA',
                            'CONTUNDENTES':'ARMA BLANCA',
                            'PUNZANTES':'ARMA BLANCA'})
C:\Users\Andres\AppData\Local\Temp\ipykernel_19416\471820810.py:1: FutureWarning:

The behavior of Series.replace (and DataFrame.replace) with CategoricalDtype is deprecated. In a future version, replace will only be used for cases that preserve the categories. To change the categories, use ser.cat.rename_categories instead.
print(df['armas_medios'].unique(), 
      df['genero'].unique(), 
      df['grupo_etario'].unique())
['ARMA BLANCA', 'ARMA DE FUEGO', 'Unknown', 'SIN EMPLEO DE ARMAS', 'ESCOPOLAMINA']
Categories (5, object): ['ARMA BLANCA', 'ARMA DE FUEGO', 'ESCOPOLAMINA', 'SIN EMPLEO DE ARMAS', 'Unknown'] ['MASCULINO', 'FEMENINO']
Categories (2, object): ['FEMENINO', 'MASCULINO'] ['ADULTOS', 'ADOLESCENTES', 'MENORES']
Categories (3, object): ['ADOLESCENTES', 'ADULTOS', 'MENORES']

Valores atípicos#

Como se vio anteriormente y dado que cada fila en el conjunto de datos representa un solo registro, se considera apropiado eliminar registros donde los casos sean más de 20, ya que es absurdo pensar que un solo incidente de violencia doméstica ha sido reportado con más de 20 víctimas de violencia en un mismo lugar. Esto podría haber sido causado por un error en la entrada de datos, ya que hay casos que superan los 100.

df[['cantidad']].query('cantidad > 20').count()
cantidad    3442
dtype: int64
df = df.query('cantidad < 20').reset_index(drop=True).copy()
fig5 = px.box(df, x = 'grupo_etario', y = 'cantidad', title="Cantidad de Casos por Grupo Etario", notched = True)
fig5
df.to_parquet(os.path.join("Data",'data_cleaned.parquet'), index=None)
df.to_csv(os.path.join("Data",'data_cleaned.csv'), index=None)